spb/company-atlas
Public
Python 66.3%
TypeScript 22.7%
JavaScript 8.6%
HTML 1.4%
CSS 0.7%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { KV, Row } from '@/components/ui/key-value';5import { Container, Note } from '@/components/ui/section';6import { api, ApiError } from '@/lib/api';7import { fmtDateTime, fmtInt } from '@/lib/format';8import { routes } from '@/lib/site';910export const revalidate = 3600;11export const metadata: Metadata = { title: 'Snapshot', robots: { index: false } };1213export default async function SnapshotPage({ params }: { params: Promise<{ id: string }> }) {14 const { id } = await params;15 let s;16 try {17 s = await api.snapshot(id);18 } catch (e) {19 if (e instanceof ApiError && e.notFound) notFound();20 throw e;21 }22 return (23 <Container>24 <div className="pb-4 pt-6 md:pt-10">25 <p className="eyebrow">Snapshot · version {s.version_no}</p>26 <h1 className="display mt-2 text-[24px] md:text-[32px]">{s.title ?? 'Normalised page version'}</h1>27 <p className="mt-2 text-sm text-ink-2">28 Fetched {fmtDateTime(s.fetched_at)} ·{' '}29 <Link href={routes.sensor(s.sensor_id)} className="link">30 sensor31 </Link>32 {s.previous_snapshot_id && (33 <>34 {' · '}35 <Link href={routes.snapshotDiff(s.previous_snapshot_id, s.id)} className="link">36 diff against previous version37 </Link>38 </>39 )}40 </p>41 </div>42 <div className="grid gap-8 lg:grid-cols-12">43 <article className="lg:col-span-8">44 <p className="eyebrow mb-2">Semantic blocks as observed</p>45 {s.blocks?.length ? (46 <ol className="space-y-2">47 {s.blocks.map((b) => (48 <li key={b.key} className="border-l-2 border-rule pl-3">49 <p className="mono text-[10px] uppercase tracking-wider text-ink-3">50 {b.kind} <span className="normal-case tracking-normal">· {b.path}</span>51 </p>52 <p className={`mt-0.5 text-sm ${b.kind === 'heading' ? 'font-semibold text-ink' : 'text-ink-2'}`}>{b.text}</p>53 </li>54 ))}55 </ol>56 ) : (57 <pre className="whitespace-pre-wrap border border-rule bg-surface-2 p-3 text-sm">{s.text}</pre>58 )}59 </article>60 <aside className="lg:col-span-4">61 <KV>62 <Row k="Language">{s.language ?? '—'}</Row>63 <Row k="Text length">64 <span className="tnum">{fmtInt(s.text_length)}</span>65 </Row>66 <Row k="Blocks">67 <span className="tnum">{fmtInt(s.block_count)}</span>68 </Row>69 <Row k="Content hash">70 <span className="mono text-xs break-all">{s.content_hash}</span>71 </Row>72 <Row k="Id">73 <span className="mono text-xs break-all">{s.id}</span>74 </Row>75 </KV>76 {Object.keys(s.extracted ?? {}).length > 0 && (77 <div className="mt-4">78 <p className="eyebrow mb-1">Extracted fields</p>79 <pre className="overflow-x-auto border border-rule bg-surface-2 p-3 text-xs">{JSON.stringify(s.extracted, null, 2)}</pre>80 </div>81 )}82 <Note className="mt-4">This is the normalised representation (scripts, styles, tracking parameters and rotating content removed). The raw object is kept in the content-addressed archive; identical pages share one object.</Note>83 </aside>84 </div>85 </Container>86 );87}88